#  Trac.py -- Library for interfacing with Trac Tickets
#  Copyright (C) 2008 Zachary Voase <cracka80@gmail.com>
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
#  02110-1301, USA.

"""ProctorTicket.Trac
Objectification of the Trac ticket system, to allow Proctor test results to
easily be inserted into a trac installation."""

from time import strftime
import md5

import Utils

try:
    from trac.env import Environment
except:
    from trac.Environment import Environment

_TICKET_FIELDS = [
    'id', 
    'type', 
    'time', 
    'changetime', 
    'component', 
    'severity', 
    'priority', 
    'owner', 
    'reporter', 
    'cc', 
    'version', 
    'milestone', 
    'status', 
    'resolution', 
    'summary', 
    'description', 
    'keywords'
]

def ValidKeys( dictIn, keyList ):
    """ValidKeys(<dictionary>, <iterable>):
Check if a dictionary's keys are all valid, with valid keys specified in the
second positional argument. Returns True/False."""
    valid_keys = set( keyList )
    keys = set( dictIn.keys() )
    return len( keys.difference( valid_keys ) ) is 0

def GetInvalidKeys( dictIn, keyList ):
    """GetInvalidKeys(<dictionary>, <iterable>):
Returns a list of the invalid key entries in the dictionary, according to valid
ones specified in the second positional argument."""
    valid_keys = set( keyList )
    keys = set( dictIn.keys() )
    return list( keys.difference( valid_keys ) )

class TracDBConn( object ):
    """TracDBConn(<string>, <preLoadDict: dictionary()>, <update: bool(True)>):
Class to provide abstractions for the Trac database connection. Upon
instantiation, accepts the path to Trac installation. By specifying a value
for preLoadDict, every time a ticket is added default options will be loaded
from the dictionary, meaning, for example, you would only need to specify
the summary and description and could have a default value for priority, etc.
The 'update' keyword, which is True by default, tells ProctorTicket that, when
adding a ticket, if it already exists, to update the changed fields instead
of raising an error."""
    def __init__( self, path, preLoadDict={}, update=True ):
        self.env = Environment( path )
        self.db = self.env.get_db_cnx()
        self.db.autocommit = False
        if not ValidKeys( preLoadDict, _TICKET_FIELDS ):
            raise KeyError( "Invalid fields in preLoadDict: %r" % \
                ( tuple( GetInvalidKeys( preLoadDict, _TICKET_FIELDS ) ), ) )
        self.preLoadDict = Utils.dictFilter( Utils.dictEmptiness, preLoadDict )
        self.update = update
    
    def checkTicket( self, value, field="summary" ):
        """TracDBConn.checkTicket(<string>, <field: "summary">):
Returns True or False, depending on whether a ticket already exists in the
database with a value for 'field' equal to the first argument."""
        assert field in _TICKET_FIELDS, "Field '%s' not valid" % ( field, )
        cur = self.db.cursor()
        SQLExpr = "SELECT COUNT(id) FROM ticket WHERE " + field + "=%s AND status<>%s"
        SQLData = [ value, 'closed' ]
        resultsProxy = cur.execute( SQLExpr, SQLData )
        if resultsProxy:
            results = resultsProxy.fetchall()
        else:
            results = cur.fetchall()
        return results[0][0] <= 0

    def getFieldValue( self, id, field ):
        """TracDBConn.getFieldValue(<integer>, <string>):
Gets the value of a specified field from a particular ticket with an ID
specified by the first positional argument."""
        assert field in _TICKET_FIELDS, "Field '%s' not valid" % ( field, )
        cursor = self.db.cursor()
        SQLExpr = "SELECT %s FROM ticket WHERE id=%s" % ( field, str(id) )
        resultsProxy = cursor.execute( SQLExpr )
        if resultsProxy is not None:
            results = resultsProxy.fetchone()
        else:
            results = cursor.fetchone()
        val = results[0]
        if val is None:
            return ''
        else:
            return val
    
    def getTicketID( self, value, field="summary" ):
        """TracDBConn.getTicketID(<string>, <field: string("summary")>):
Returns the 'id' field for the first record with a 'field' value equal
to the first positional argument."""
        assert field in _TICKET_FIELDS, "Field '%s' not valid" % ( field, )
        cur = self.db.cursor()
        SQLExpr = "SELECT id FROM ticket WHERE " + field + "=%s"
        SQLData = [ str( value ) ]
        resultsProxy = cur.execute( SQLExpr, SQLData )
        if resultsProxy:
            results = resultsProxy.fetchone()
        else:
            results = cur.fetchone()
        if len( results ) is 0:
            return -1
        else:
            return results[0]
    
    def addTicket( self, ticketDict, preLoad=True ):
        """TracDBConn.addTicket(<dictionary>, <preLoad: boolean(True)>):
Takes the dictionary given, makes sure all keys are valid, and inserts the
specified ticket into the database. Returns the id of the ticket created.
Also loads data for each ticket from the 'preLoadDict' attribute, unless 
preLoad=False is specified as an optional argument."""
        if not ValidKeys( ticketDict, _TICKET_FIELDS ):
            raise KeyError( "Invalid fields in ticketDict: %r" % \
                ( tuple( GetInvalidKeys( ticketDict, _TICKET_FIELDS ) ), ) )
        SQLDict = {}
        if preLoad:
            for key, value in self.preLoadDict.items():
                SQLDict[key] = value
        for key, value in ticketDict.items():
            SQLDict[key] = value
        if 'changetime' not in SQLDict:
            SQLDict['changetime'] = SQLDict['time']
        if 'keywords' in SQLDict:
            SQLDict['keywords'] = " ".join( SQLDict['keywords'] )
        cur = self.db.cursor()
        SQLExpr = "INSERT INTO ticket ( %s ) VALUES ( %s )" % \
            ( ", ".join( SQLDict.keys() ), " ,".join( ["%s"]*len( SQLDict.values() ) ) )
        SQLData = map( str, SQLDict.values() )
        cur.execute( SQLExpr, SQLData )
        self.db.commit()
        return self.db.get_last_id( cur, 'ticket' )

    def getTicket( self, ticketID ):
        """TracDBConn.getTicket(<integer>):
Returns a dictionary with all of the values from a particular ticket."""
        dictOut = {}
        for key in _TICKET_FIELDS:
            dictOut[key] = self.getFieldValue( ticketID, key )
        return Utils.dictFilter( Utils.dictEmptiness, dictOut )
    
    def updateTicket( self, ticketID, changeDict, author="" ):
        """TracDBConn.updateTicket(<integer>, <dictionary>, <author: string()>):
This accepts a ticket ID and a dictionary of changed values, and updates the
database with new values for that ID. It also adds records to ticket_change,
so that changes are logged. The dictionary argument does not have to include
time, but if it is it will override the default action of getting the current
time from time.localtime(). The 'author' keyword specifies who should be the
author of the change; by default this field takes the value of the reporter,
or else the owner, of the ticket."""
        oldTicket = self.getTicket( ticketID )
        if not ValidKeys( changeDict, _TICKET_FIELDS ):
            raise KeyError( "Invalid fields in update dictionary: %r" % \
                ( tuple( GetInvalidKeys( changeDict, _TICKET_FIELDS ) ), ) )
        updateCursor = self.db.cursor()
        SETStmt = ", ".join( [ key + "=%s" for key in changeDict.keys() ] )
        USQLExpr = "UPDATE ticket SET " + SETStmt + " WHERE id=%s"
        USQLData = map( str, changeDict.values() ) + [ str( ticketID ) ]
        updateCursor.execute( USQLExpr, USQLData )
        insertCursor = self.db.cursor()
        ISQLQueue = []
        if author == "":
            author = changeDict.get( "reporter", changeDict.get( "owner", self.preLoadDict.get("reporter", self.preLoadDict.get("owner"))))
        for key, value in changeDict.items():
            ISQLQuery = []
            if key in [ "time", "changetime" ]:
                continue
            else:
                oldvalue = oldTicket.get(key, "")
                if unicode(oldvalue) == u'':
                    oldvalue = None
                if unicode(oldvalue) == unicode(value):
                    continue
                ISQLQuery = []
                if not author:
                    if oldvalue is None:
                        ISQLQuery.append("INSERT INTO ticket_change (ticket, time, field, newvalue) VALUES ( %s, %s, %s, %s )")
                        ISQLQuery.append( [ str( ticketID ), changeDict.get( "time", strftime( "%s" ) ), key, value ] )
                    else:
                        ISQLQuery.append("INSERT INTO ticket_change (ticket, time, field, oldvalue, newvalue) VALUES ( %s, %s, %s, %s, %s )")
                        ISQLQuery.append( [ str( ticketID ), changeDict.get( "time", strftime( "%s" ) ), key, oldvalue, value ] )
                else:
                    if oldvalue is None:
                        ISQLQuery.append("INSERT INTO ticket_change (ticket, time, author, field, newvalue) VALUES ( %s, %s, %s, %s, %s )")
                        ISQLQuery.append( [ str( ticketID ), changeDict.get( "time", strftime( "%s" ) ), author, key, value ] )
                    else:
                        ISQLQuery.append("INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) VALUES ( %s, %s, %s, %s, %s, %s )")
                        ISQLQuery.append( [ str( ticketID ), changeDict.get( "time", strftime( "%s" ) ), author, key, oldvalue, value ] )
            ISQLQueue.append(ISQLQuery)
        for Query in ISQLQueue:
            insertCursor.execute(*Query)
        self.db.commit()
    
    def checkAndAddOrUpdate( self, ticketDict, field="summary", preLoad=True ):
        """TracDBConn.checkAndAddOrUpdate(<dictionary>, <field: string("summary")>, <preLoad: True>)
Makes sure that a ticket is not already in the database and then adds it and
returns the new record's 'id' field.
If it is in the database already then the program tries to see whether the
description field has changed. If it has then it performs an update."""
        if self.checkTicket( ticketDict[field], field=field ):
            return ( self.addTicket( ticketDict, preLoad=preLoad ), "add" )
        elif not self.update:
            raise AssertionError( "Ticket '%s' already exists" % ( ticketDict["summary"], ) )
        else:
            ticketID = self.getTicketID( ticketDict["summary"] )
            oldDesc = self.getFieldValue(ticketID, "description")
            oldDescSet = set( oldDesc.split( "-"*20 ) )
            newDesc = ticketDict["description"]
            newDescSet = set( newDesc.split( "-"*20 ) )
            if len( newDescSet.symmetric_difference( oldDescSet ) ) > 0:
                author = ticketDict.get("reporter", ticketDict.get("author", ""))
                self.updateTicket( ticketID, ticketDict, author=author )
                return ( ticketID, "update" )
            else:
                raise AssertionError( "Ticket '%s' already exists" % ( ticketDict["summary"], ) )
